// Tests the basic API of the getDefaultRWConcern and setDefaultRWConcern commands and their
// associated persisted state against different topologies.
// @tags: [
//    # TODO (SERVER-97257): Re-enable this test or add an explanation why it is incompatible.
//    embedded_router_incompatible,
// ]
// Asserts a set/get default RWC command response or persisted document contains the expected
// fields. Assumes a default read or write concern has been set previously and the response was not
// generated by a getDefaultRWConcern command with inMemory=true.

import {ReplSetTest} from "jstests/libs/replsettest.js";
import {ShardingTest} from "jstests/libs/shardingtest.js";

// This test requires running commands directly against the shard.
TestData.replicaSetEndpointIncompatible = true;

function verifyFields(res, {expectRC, expectWC, isPersistedDocument}, isImplicitDefaultWCMajority) {
    // These fields are always set once a read or write concern has been set at least once.
    let expectedFields = ["updateOpTime", "updateWallClockTime", "localUpdateWallClockTime"];
    let unexpectedFields = ["inMemory"];

    if (expectRC || !isPersistedDocument) {
        expectedFields.push("defaultReadConcern");
    } else {
        unexpectedFields.push("defaultReadConcern");
    }

    if (!isPersistedDocument) {
        expectedFields.push("defaultReadConcernSource");
    } else {
        unexpectedFields.push("defaultReadConcernSource");
    }

    if (expectWC || (isImplicitDefaultWCMajority && !isPersistedDocument)) {
        expectedFields.push("defaultWriteConcern");
    } else {
        unexpectedFields.push("defaultWriteConcern");
    }

    if (!isPersistedDocument) {
        expectedFields.push("defaultWriteConcernSource");
    } else {
        unexpectedFields.push("defaultWriteConcernSource");
    }

    // localUpdateWallClockTime is generated by the in-memory cache and is not stored in the
    // persisted document.
    if (isPersistedDocument) {
        expectedFields = expectedFields.filter(field => field !== "localUpdateWallClockTime");
        unexpectedFields.push("localUpdateWallClockTime");
    }

    assert.hasFields(res, expectedFields);
    unexpectedFields.forEach(field => {
        assert(!res.hasOwnProperty(field),
               `response unexpectedly had field '${field}', res: ${tojson(res)}`);
    });
    if (!isPersistedDocument) {
        if (expectWC) {
            assert.eq(res.defaultWriteConcernSource, "global", tojson(res));
        } else {
            assert.eq(res.defaultWriteConcernSource, "implicit", tojson(res));
        }
    }
}

function verifyDefaultRWCommandsInvalidInput(conn) {
    //
    // Test invalid parameters for getDefaultRWConcern.
    //

    // Invalid inMemory.
    assert.commandFailedWithCode(conn.adminCommand({getDefaultRWConcern: 1, inMemory: "true"}),
                                 ErrorCodes.TypeMismatch);

    //
    // Test invalid parameters for setDefaultRWConcern.
    //

    // Must include either wc or rc.
    assert.commandFailedWithCode(conn.adminCommand({setDefaultRWConcern: 1}), ErrorCodes.BadValue);

    // Invalid write concern.
    assert.commandFailedWithCode(
        conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: 1}),
        ErrorCodes.TypeMismatch);

    // w less than 1.
    assert.commandFailedWithCode(conn.adminCommand({
        setDefaultRWConcern: 1,
        defaultWriteConcern: {w: 0},
    }),
                                 ErrorCodes.BadValue);

    // Empty write concern is not allowed if write concern has already been set.
    assert.commandFailedWithCode(
        conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {}}),
        ErrorCodes.IllegalOperation);

    // Invalid read concern.
    assert.commandFailedWithCode(conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: 1}),
                                 ErrorCodes.TypeMismatch);

    // Non-existent level.
    assert.commandFailedWithCode(
        conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "dummy"}}),
        [ErrorCodes.FailedToParse, ErrorCodes.BadValue]);

    // Unsupported level.
    assert.commandFailedWithCode(
        conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "linearizable"}}),
        ErrorCodes.BadValue);
    assert.commandFailedWithCode(
        conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "snapshot"}}),
        ErrorCodes.BadValue);

    // Fields other than level.
    assert.commandFailedWithCode(conn.adminCommand({
        setDefaultRWConcern: 1,
        defaultReadConcern: {level: "local", afterClusterTime: Timestamp(50, 1)}
    }),
                                 ErrorCodes.BadValue);
    assert.commandFailedWithCode(conn.adminCommand({
        setDefaultRWConcern: 1,
        defaultReadConcern: {level: "snapshot", atClusterTime: Timestamp(50, 1)}
    }),
                                 ErrorCodes.BadValue);
    assert.commandFailedWithCode(conn.adminCommand({
        setDefaultRWConcern: 1,
        defaultReadConcern: {level: "local", afterOpTime: {ts: Timestamp(50, 1), t: 1}}
    }),
                                 ErrorCodes.BadValue);
}

// Verifies the default responses for the default RWC commands and the default persisted state.
function verifyDefaultState(conn, isImplicitDefaultWCMajority) {
    const res = assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1}));
    const inMemoryRes =
        assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true}));

    // localUpdateWallClockTime is set when a node refreshes its defaults, even if none are found.
    const expectedFields = ["localUpdateWallClockTime"];
    if (isImplicitDefaultWCMajority) {
        expectedFields.push("defaultWriteConcern");
    }

    expectedFields.push("defaultWriteConcernSource");
    expectedFields.push("defaultReadConcern");
    expectedFields.push("defaultReadConcernSource");

    expectedFields.forEach(field => {
        assert(res.hasOwnProperty(field),
               `response did not have field '${field}', res: ${tojson(res)}`);
        assert(inMemoryRes.hasOwnProperty(field),
               `inMemory=true response did not have field '${field}', res: ${tojson(inMemoryRes)}`);
    });
    assert.eq(inMemoryRes.inMemory, true, tojson(inMemoryRes));

    assert.eq(res.defaultWriteConcernSource, "implicit", tojson(res));
    assert.eq(inMemoryRes.defaultWriteConcernSource, "implicit", tojson(inMemoryRes));

    // No other fields should be returned if neither a default read nor write concern has been set.
    const unexpectedFields = ["updateOpTime", "updateWallClockTime"];
    if (!isImplicitDefaultWCMajority) {
        unexpectedFields.push("defaultWriteConcern");
    }

    unexpectedFields.forEach(field => {
        assert(!res.hasOwnProperty(field),
               `response unexpectedly had field '${field}', res: ${tojson(res)}`);
        assert(!inMemoryRes.hasOwnProperty(field),
               `inMemory=true response unexpectedly had field '${field}', res: ${
                   tojson(inMemoryRes)}`);
    });
    assert.eq(undefined, res.inMemory, tojson(res));

    // There should be no default RWC document.
    assert.eq(null, getPersistedRWCDocument(conn));
}

function verifyDefaultRWCommandsValidInputOnSuccess(conn, isImplicitDefaultWCMajority) {
    //
    // Test getDefaultRWConcern when neither read nor write concern are set.
    //

    // No parameters is allowed.
    assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1}));

    // inMemory parameter is allowed.
    assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true}));
    assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: false}));

    // An inMemory response should contain inMemory=true.
    const inMemoryRes =
        assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1, inMemory: true}));
    assert.eq(inMemoryRes.inMemory, true, tojson(inMemoryRes));

    //
    // Test getting and setting read concern.
    //

    // Test setDefaultRWConcern when only read concern is set.
    verifyFields(assert.commandWorked(conn.adminCommand(
                     {setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}})),
                 {expectRC: true, expectWC: false},
                 isImplicitDefaultWCMajority);
    verifyFields(getPersistedRWCDocument(conn),
                 {expectRC: true, expectWC: false, isPersistedDocument: true},
                 isImplicitDefaultWCMajority);

    // Test getDefaultRWConcern when only read concern is set.
    verifyFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
                 {expectRC: true, expectWC: false},
                 isImplicitDefaultWCMajority);

    // Test unsetting read concern.
    verifyFields(
        assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {}})),
        {expectRC: false, expectWC: false},
        isImplicitDefaultWCMajority);
    verifyFields(getPersistedRWCDocument(conn),
                 {expectRC: false, expectWC: false, isPersistedDocument: true},
                 isImplicitDefaultWCMajority);
    verifyFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
                 {expectRC: false, expectWC: false},
                 isImplicitDefaultWCMajority);

    //
    // Test getting and setting write concern.
    //

    // Empty write concern is allowed if write concern has not already been set.
    verifyFields(
        assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {}})),
        {expectRC: false, expectWC: false},
        isImplicitDefaultWCMajority);
    verifyFields(getPersistedRWCDocument(conn),
                 {expectRC: false, expectWC: false, isPersistedDocument: true},
                 isImplicitDefaultWCMajority);

    // Test setRWConcern when only write concern is set.
    assert.commandWorked(conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}}));
    assert.commandWorked(
        conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: 1, j: false}}));
    assert.commandWorked(
        conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: "majority"}}));

    verifyFields(assert.commandWorked(
                     conn.adminCommand({setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}})),
                 {expectRC: false, expectWC: true},
                 isImplicitDefaultWCMajority);
    verifyFields(getPersistedRWCDocument(conn),
                 {expectRC: false, expectWC: true, isPersistedDocument: true},
                 isImplicitDefaultWCMajority);

    // Test getRWConcern when only write concern is set.
    verifyFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
                 {expectRC: false, expectWC: true},
                 isImplicitDefaultWCMajority);

    //
    // Test getting and setting both read and write concern.
    //
    verifyFields(assert.commandWorked(conn.adminCommand({
        setDefaultRWConcern: 1,
        defaultReadConcern: {level: "local"},
        defaultWriteConcern: {w: 1}
    })),
                 {expectRC: true, expectWC: true},
                 isImplicitDefaultWCMajority);
    verifyFields(getPersistedRWCDocument(conn),
                 {expectRC: true, expectWC: true, isPersistedDocument: true},
                 isImplicitDefaultWCMajority);

    // Test getRWConcern when both read and write concern are set.
    verifyFields(assert.commandWorked(conn.adminCommand({getDefaultRWConcern: 1})),
                 {expectRC: true, expectWC: true},
                 isImplicitDefaultWCMajority);
}

function getPersistedRWCDocument(conn) {
    return conn.getDB("config").settings.findOne({_id: "ReadWriteConcernDefaults"});
}

// Verifies the error code returned by connections to nodes that do not support the get/set default
// rw concern commands.
function verifyDefaultRWCommandsFailWithCode(conn, {failureCode}) {
    assert.commandFailedWithCode(conn.adminCommand({getDefaultRWConcern: 1}), failureCode);
    assert.commandFailedWithCode(
        conn.adminCommand({setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}),
        failureCode);
}

jsTestLog("Testing sharded cluster with implicit default write concern majority...");
{
    let st = new ShardingTest({shards: 2, rs: {nodes: 2}});

    // Mongos succeeds.
    verifyDefaultState(st.s, true /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsValidInputOnSuccess(st.s, true /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsInvalidInput(st.s);

    // Shard node fails.
    verifyDefaultRWCommandsFailWithCode(st.rs1.getPrimary(), {failureCode: 51301});
    assert.commandFailedWithCode(st.rs1.getSecondary().adminCommand({getDefaultRWConcern: 1}),
                                 51301);
    // Secondaries fail setDefaultRWConcern before executing the command.
    assert.commandFailedWithCode(
        st.rs1.getSecondary().adminCommand(
            {setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}),
        ErrorCodes.NotWritablePrimary);

    st.stop();
    st = new ShardingTest({shards: 1, rs: {nodes: 2}});
    // Config server primary succeeds.
    verifyDefaultState(st.configRS.getPrimary(), true /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsValidInputOnSuccess(st.configRS.getPrimary(),
                                               true /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsInvalidInput(st.configRS.getPrimary());

    // Config server secondary can run getDefaultRWConcern, but not setDefaultRWConcern.
    assert.commandWorked(st.configRS.getSecondary().adminCommand({getDefaultRWConcern: 1}));
    assert.commandFailedWithCode(
        st.configRS.getSecondary().adminCommand(
            {setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}),
        ErrorCodes.NotWritablePrimary);

    st.stop();
}

if (jsTestOptions().useAutoBootstrapProcedure) {  // TODO: SERVER-80318 Delete tests below
    quit();
}

jsTestLog("Testing standalone mongod...");
{
    const standalone = MongoRunner.runMongod();

    // Standalone node fails.
    verifyDefaultRWCommandsFailWithCode(standalone, {failureCode: 51300});

    MongoRunner.stopMongod(standalone);
}

jsTestLog("Testing standalone replica set with implicit default write concern majority...");
{
    const rst = new ReplSetTest({nodes: 2});
    rst.startSet();
    rst.initiate();

    // Primary succeeds.
    const primary = rst.getPrimary();

    verifyDefaultState(primary, true /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsValidInputOnSuccess(primary, true /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsInvalidInput(primary);

    // Secondary can run getDefaultRWConcern, but not setDefaultRWConcern.
    assert.commandWorked(rst.getSecondary().adminCommand({getDefaultRWConcern: 1}));
    assert.commandFailedWithCode(
        rst.getSecondary().adminCommand(
            {setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}),
        ErrorCodes.NotWritablePrimary);

    rst.stopSet();
}

jsTestLog("Testing standalone replica set with implicit default write concern {w:1}...");
{
    const rst = new ReplSetTest({nodes: [{}, {}, {arbiter: true}]});
    rst.startSet();
    rst.initiate();

    // Primary succeeds.
    const primary = rst.getPrimary();

    verifyDefaultState(primary, false /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsValidInputOnSuccess(primary, false /* isImplicitDefaultWCMajority */);
    verifyDefaultRWCommandsInvalidInput(primary);

    // Secondary can run getDefaultRWConcern, but not setDefaultRWConcern.
    assert.commandWorked(rst.getSecondary().adminCommand({getDefaultRWConcern: 1}));
    assert.commandFailedWithCode(
        rst.getSecondary().adminCommand(
            {setDefaultRWConcern: 1, defaultReadConcern: {level: "local"}}),
        ErrorCodes.NotWritablePrimary);

    rst.stopSet();
}
